本文首发于奇安信攻防社区:https://forum.butian.net/share/573

漏洞原理

漏洞的利用链前半部分和CVE-2020-26217相同,思路也很像,都是利用Base64Data.get方法,替换一个恶意的InputStream,区别在于之前的入口被封了,因此使用PriorityQueue+ObservableList$1的方法调用到Base64Data的toString。需要注意的是ObservableList这个类只有较高版本JDK会创建内部匿名的compartor。xstream的反序列化漏洞一个很重要的特点是可以利用没有实现Serializable接口的类。

利用链如下

1
2
3
4
5
6
7
8
9
10
11
12
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingCompartor
javaFx.collections.ObservableList$1
com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data.toString
com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data.get
com.sun.xml.internal.bind.v2.util.ByteArrayOutputStreamEx.readFrom
while(True)
...


核心的利用点在于ByteArrayOutputStreamEx的 readFrom()方法,该方法while(true)进行条件判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void readFrom(InputStream is) throws IOException {
while(true) {
if (this.count == this.buf.length) {
byte[] data = new byte[this.buf.length * 2];
System.arraycopy(this.buf, 0, data, 0, this.buf.length);
this.buf = data;
}

int sz = is.read(this.buf, this.count, this.buf.length - this.count);
if (sz < 0) {
return;
}

this.count += sz;
}
}

在read函数中,会计算count-pos值,并且如果len大于这个值时,就会判断这个值是否大于0,如果小于等于0则返回0,这里本来的作用是是计算是否已经读完数据。但是如果将pos设置为int类型的最小值,count设置为0,那么count-pos则会等于pos。这和int类型在Java中的存储方式有关,java中最大的正数是2147483647,存储成二进制格式就是011111111111111111111111111111111,而如果此时再加1,则会变成10000000000000000000000,这其实是Java中int类型最小值-2147483648所对应的二进制数,同样的,0减一个负数,实际的数值等于这个负数取反并加一, 10000000000000000000000取反为011111111111111111111111111111111,再加一就又变成了最小的负数。从而陷入了readFrom的循环中。

POC构造

在poc的构造中,如果试图直接从结果入手,演算xstream的处理流程,会比较复杂,简单的方法是构造出对应的对象,然后再调用toXML即可获得恶意XML文本。构造的过程如下:

确定利用链→构造出对应的对象→调用toXML方法

Source

首先确定一个可以利用的入手点,漏洞作者找到的是PriorityQueue,xstream会调用这个类的readObject方法。

Gadget

在PriorityQueue的利用中,需要找一个危险的comparator。作者找的这个comparator很独特,它是一个匿名类,藏在javafx.collections.ObservableList这个类的一个方法中,在Java中,就算是匿名类,在运行中也会存储一个类名,按照在代码中的定义顺序,在外部类名后增加$1、$2等依次排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public default SortedList<E> sorted() {
Comparator naturalOrder = new Comparator<E>() {

@Override
public int compare(E o1, E o2) {
if (o1 == null && o2 == null) {
return 0;
}
if (o1 == null) {
return -1;
}
if (o2 == null) {
return 1;
}

if (o1 instanceof Comparable) {
return ((Comparable) o1).compareTo(o2);
}

return Collator.getInstance().compare(o1.toString(), o2.toString());
}
};
return sorted(naturalOrder);
}

在这个类中只有这一个匿名类,因此这个匿名类的名字就是javafx.collections.ObservableList$1,利用Class.forName可以找到这个类。接下来是实例化这个类,在Xstream中对类的实例化使用的是Unsafe类的allocateInstance方法,这个方法可以在不使用构造方法的情况下直接实例化对象,这里构造poc也同样可以使用这个方法。

这个类的compare调用了比较对象的toString方法,下一步是找一个可用的toString方法,作者使用的是Base64Data这个类,这个类不是第一次出现了,在Xstream2020年爆出的3个漏洞中都使用了它。由于入口被封,因此这里换了一个方法进行触发。它的特点是toString→get→ByteArrayOutputStreamEx.readFrom()

ByteArrayOutputStreamEx.readFrom()中使用了while(True)读取数据,is.read()返回0,则循环永远不会结束。